package io

import (
	"fmt"
	"gitee.com/flyspace81/secure-cli/base"
	"io/ioutil"
	"math/rand"
	"os"
	"os/user"
	"path/filepath"
	"runtime"
	"strings"
	"time"
)

type FilterOptionType struct {
	Name    string
	Pattern string
}

type FileInfoType struct {
	FileName string
	FilePath string
	Dir      string
}

func getCurrentDirFileList(dpath string, chFiles chan<- FileInfoType, filters []FilterOptionType) error {
	if !strings.HasSuffix(dpath, string(os.PathSeparator)) {
		dpath += string(os.PathSeparator)
	}

	fileList, err := ioutil.ReadDir(dpath)
	if err != nil {
		return err
	}

	for _, fileInfo := range fileList {
		if !fileInfo.IsDir() {
			realInfo, errF := os.Stat(dpath + fileInfo.Name())
			if errF == nil && realInfo.IsDir() {
				// for symlink
				continue
			}

			if doesSingleFileMatchPatterns(fileInfo.Name(), filters) {
				chFiles <- FileInfoType{GetFileName(fileInfo.Name()), fileInfo.Name(), dpath}
			}
		}
	}
	return nil
}

func containsInStrsSlice(vs []string, t string) bool {
	if len(vs) == 0 {
		return false
	}

	for _, v := range vs {
		if v == t {
			return true
		}
	}
	return false
}

func filterSingleStr(v, p string, include bool) bool {
	_, name := filepath.Split(v)
	res, _ := filepath.Match(p, name)

	if include {
		return res
	} else {
		return !res
	}
}

func filterStrsWithInclude(vs []string, p string) []string {
	vsf := make([]string, 0)
	for _, v := range vs {
		_, name := filepath.Split(v)
		res, _ := filepath.Match(p, name)
		if res {
			vsf = append(vsf, v)
		}

	}
	return vsf
}

func filterStrsWithExclude(vs []string, p string) []string {
	vsf := make([]string, 0)
	for _, v := range vs {
		_, name := filepath.Split(v)
		res, _ := filepath.Match(p, name)
		if !res {
			vsf = append(vsf, v)
		}

	}
	return vsf
}

func matchFiltersForStr(str string, filters []FilterOptionType) bool {
	if len(filters) == 0 {
		return true
	}

	var res bool
	if filters[0].Name == base.IncludePrompt {
		res = filterSingleStr(str, filters[0].Pattern, true)
	} else {
		res = filterSingleStr(str, filters[0].Pattern, false)
	}

	for _, filter := range filters[1:] {
		if filter.Name == base.IncludePrompt {
			res = res || filterSingleStr(str, filter.Pattern, true)
		} else {
			res = res && filterSingleStr(str, filter.Pattern, false)
		}
	}

	return res
}

func matchFiltersForStrs(strs []string, filters []FilterOptionType) []string {
	if len(filters) == 0 {
		return strs
	}

	vsf := make([]string, 0)

	for _, str := range strs {
		if matchFiltersForStr(str, filters) {
			vsf = append(vsf, str)
		}
	}

	return vsf
}

func matchFiltersForStrsInArray(strs []string, filters []FilterOptionType) []string {
	if len(filters) == 0 {
		return strs
	}
	vsf := make([]string, 0)

	if filters[0].Name == base.IncludePrompt {
		vsf = append(vsf, filterStrsWithInclude(strs, filters[0].Pattern)...)
	} else {
		vsf = append(vsf, filterStrsWithExclude(strs, filters[0].Pattern)...)
	}

	for _, filter := range filters[1:] {
		if filter.Name == base.IncludePrompt {
			vsf = append(vsf, filterStrsWithInclude(strs, filter.Pattern)...)
		} else {
			vsf = filterStrsWithExclude(vsf, filter.Pattern)
		}
	}

	return vsf
}

// Following for files

func doesSingleFileMatchPatterns(filename string, filters []FilterOptionType) bool {
	if len(filters) == 0 {
		return true
	}

	files := []string{filename}
	vsf := matchFiltersForStrs(files, filters)

	if len(vsf) > 0 {
		return true
	}
	return false
}

func containsInFileSlice(vs []FileInfoType, t FileInfoType) bool {
	if len(vs) == 0 {
		return false
	}

	for _, v := range vs {
		if v == t {
			return true
		}
	}
	return false
}

func filterFilesWithInclude(vs []FileInfoType, p string) []FileInfoType {
	vsf := make([]FileInfoType, 0)
	for _, v := range vs {
		_, filename := filepath.Split(v.FilePath)
		res, _ := filepath.Match(p, filename)

		if res {
			vsf = append(vsf, v)
		}

	}
	return vsf
}

func filterFilesWithExclude(vs []FileInfoType, p string) []FileInfoType {
	vsf := make([]FileInfoType, 0)
	for _, v := range vs {
		_, filename := filepath.Split(v.FilePath)
		res, _ := filepath.Match(p, filename)
		if !res {
			vsf = append(vsf, v)
		}

	}
	return vsf
}

// Following for objects

func doesSingleObjectMatchPatterns(object string, filters []FilterOptionType) bool {
	if len(filters) == 0 {
		return true
	}

	return doesSingleFileMatchPatterns(object, filters)
}

var letters = []rune("0123456789abcdefghijklmnopqrstuvwxyz")

func randStr(n int) string {
	b := make([]rune, n)
	r := rand.New(rand.NewSource(time.Now().UnixNano()))
	for i := range b {
		b[i] = letters[r.Intn(len(letters))]
	}
	return string(b)
}

func randLowStr(n int) string {
	return strings.ToLower(randStr(n))
}

func getFileList(dpath string) ([]string, error) {
	fileNames := make([]string, 0)
	err := filepath.Walk(dpath, func(fpath string, f os.FileInfo, err error) error {
		if f == nil {
			return err
		}
		dpath = filepath.Clean(dpath)
		fpath = filepath.Clean(fpath)
		if err != nil {
			return fmt.Errorf("list file error: %s, info: %s", fpath, err.Error())
		}

		// fpath may be dir,exclude itself
		if fpath != dpath {
			fileNames = append(fileNames, fpath)
		}

		return nil
	})
	return fileNames, err
}

func currentHomeDir() string {
	homeDir := ""
	homeDrive := os.Getenv("HOMEDRIVE")
	homePath := os.Getenv("HOMEPATH")
	if runtime.GOOS == "windows" && homeDrive != "" && homePath != "" {
		homeDir = homeDrive + string(os.PathSeparator) + homePath
	}

	if homeDir != "" {
		return homeDir
	}

	usr, _ := user.Current()
	if usr != nil {
		homeDir = usr.HomeDir
	} else {
		homeDir = os.Getenv("HOME")
	}
	return homeDir
}

func getCurrentDirFileListCommon(dpath string, chFiles chan<- FileInfoType, filters []FilterOptionType) error {
	if !strings.HasSuffix(dpath, string(os.PathSeparator)) {
		dpath += string(os.PathSeparator)
	}

	fileList, err := ioutil.ReadDir(dpath)
	if err != nil {
		return err
	}

	for _, fileInfo := range fileList {
		if !fileInfo.IsDir() {
			realInfo, errF := os.Stat(dpath + fileInfo.Name())
			if errF == nil && realInfo.IsDir() {
				// for symlink
				continue
			}

			if doesSingleFileMatchPatterns(fileInfo.Name(), filters) {
				chFiles <- FileInfoType{GetFileName(fileInfo.Name()), fileInfo.Name(), dpath}
			}
		}
	}
	return nil
}

func GetFileListCommon(dpath string, chFiles chan<- FileInfoType, onlyCurrentDir bool, disableAllSymlink bool,
	enableSymlinkDir bool, enableFilterDir bool, filters []FilterOptionType) error {
	defer close(chFiles)
	if onlyCurrentDir {
		return getCurrentDirFileListCommon(dpath, chFiles, filters)
	}

	name := dpath
	symlinkDiretorys := []string{dpath}
	walkFunc := func(fpath string, f os.FileInfo, err error) error {
		if f == nil {
			return err
		}

		dpath = filepath.Clean(dpath)
		fpath = filepath.Clean(fpath)

		fileName, err := filepath.Rel(dpath, fpath)
		if err != nil {
			return fmt.Errorf("list file error: %s, info: %s", fpath, err.Error())
		}

		if f.IsDir() {
			if fpath != dpath && !enableFilterDir {
				if strings.HasSuffix(fileName, "\\") || strings.HasSuffix(fileName, "/") {
					chFiles <- FileInfoType{"", fileName, name}
				} else {
					chFiles <- FileInfoType{"", fileName + string(os.PathSeparator), name}
				}
			}
			return nil
		}

		if disableAllSymlink && (f.Mode()&os.ModeSymlink) != 0 {
			return nil
		}

		if enableSymlinkDir && (f.Mode()&os.ModeSymlink) != 0 {
			// there is difference between os.Stat and os.Lstat in filepath.Walk
			realInfo, err := os.Stat(fpath)
			if err != nil {
				return err
			}

			if realInfo.IsDir() {
				// it's symlink dir
				// if linkDir has suffix os.PathSeparator,os.Lstat determine it is a dir
				if !strings.HasSuffix(name, string(os.PathSeparator)) {
					name += string(os.PathSeparator)
				}
				linkDir := name + fileName + string(os.PathSeparator)
				symlinkDiretorys = append(symlinkDiretorys, linkDir)
				return nil
			}
		}

		if doesSingleFileMatchPatterns(fileName, filters) {
			chFiles <- FileInfoType{GetFileName(fileName), fileName, name}
		}
		return nil
	}

	var err error
	for {
		symlinks := symlinkDiretorys
		symlinkDiretorys = []string{}
		for _, v := range symlinks {
			err = filepath.Walk(v, walkFunc)
			if err != nil {
				return err
			}
		}
		if len(symlinkDiretorys) == 0 {
			break
		}
	}
	return err
}

func GetFileName(t string) string {
	nstr := strings.ReplaceAll(t, base.FileWinSep, base.FileLInuxSep)
	index := strings.LastIndex(nstr, base.FileLInuxSep)
	retstr := nstr[index+1:]
	return retstr
}

func GetFileSuffix(t string) string {
	index := strings.LastIndex(t, base.FileSpot)
	retstr := t[index+1:]
	return retstr
}
